package org.net5ijy.cloud.plugin.feign.core;

import org.net5ijy.cloud.plugin.feign.core.model.FeignClientClass;
import org.net5ijy.cloud.plugin.feign.core.model.FeignMethod;
import org.net5ijy.cloud.plugin.feign.core.model.FeignMethodArgument;
import org.net5ijy.cloud.plugin.feign.core.util.ClazzUtil;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.net5ijy.cloud.plugin.feign.core.util.SpringControllerUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * FeignClientScanner
 *
 * @author XGF
 * @date 2020/11/20 14:30
 */
public class FeignClientScanner {

  private Set<String> modelClassNames = new HashSet<>();

  private String modelScanPath;

  public FeignClientScanner(String modelScanPath) {
    this.modelScanPath = modelScanPath;
  }

  public List<FeignClientClass> scan(String scanPackage) {

    List<String> clazzList = ClazzUtil.getClazzName(scanPackage, true);

    List<FeignClientClass> list = new ArrayList<>();

    for (String clazzName : clazzList) {

      // 反射获取controller类
      try {

        Class<?> clazz = Class.forName(clazzName);

        // 判断是否是一个controller
        Controller controller = clazz.getAnnotation(Controller.class);

        if (controller == null) {
          RestController restController = clazz.getAnnotation(RestController.class);
          if (restController == null) {
            continue;
          }
        }

        FeignClientClass feignClientClass = resolveFeignClientClass(clazz);

        list.add(feignClientClass);

      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      }
    }

    return list;
  }

  private FeignClientClass resolveFeignClientClass(Class<?> clazz) {

    RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);

    if (requestMapping == null) {
      return null;
    }

    // 创建一个FeignClientClass
    FeignClientClass feignClientClass = new FeignClientClass();

    // 获取类名
    String clazzSimpleName = clazz.getSimpleName();

    // 获取feign接口名
    String feignName = clazzSimpleName.replaceAll("Controller", "");
    // 设置feign类名
    feignClientClass.setFeignClassName(feignName + "Feign");

    // 获取url
    String url = SpringControllerUtil.resolveUrlFromRequestMapping(requestMapping);

    feignClientClass.setUrl(url);

    // 获取所有方法
    Method[] methods = clazz.getMethods();

    // 保存这个feign下面的所有接口方法
    List<FeignMethod> feignMethodList = new ArrayList<>();

    for (Method method : methods) {
      FeignMethod feignMethod = resolveFeignMethodFromJavaMethod(method);
      if (feignMethod != null) {
        feignMethodList.add(feignMethod);
      }
    }

    feignClientClass.setMethods(feignMethodList);

    return feignClientClass;
  }

  private FeignMethod resolveFeignMethodFromJavaMethod(Method method) {

    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);

    String url = null;
    String httpMethod = null;

    if (requestMapping == null) {
      if (method.getAnnotation(GetMapping.class) != null) {
        GetMapping getMapping = method.getAnnotation(GetMapping.class);
        url = SpringControllerUtil.resolveUrlFromGetMapping(getMapping);
        httpMethod = "GET";
      } else if (method.getAnnotation(PostMapping.class) != null) {
        PostMapping postMapping = method.getAnnotation(PostMapping.class);
        url = SpringControllerUtil.resolveUrlFromPostMapping(postMapping);
        httpMethod = "POST";
      } else if (method.getAnnotation(PutMapping.class) != null) {
        PutMapping putMapping = method.getAnnotation(PutMapping.class);
        url = SpringControllerUtil.resolveUrlFromPutMapping(putMapping);
        httpMethod = "PUT";
      } else if (method.getAnnotation(DeleteMapping.class) != null) {
        DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
        url = SpringControllerUtil.resolveUrlFromDeleteMapping(deleteMapping);
        httpMethod = "DELETE";
      }
    } else {
      url = SpringControllerUtil.resolveUrlFromRequestMapping(requestMapping);
      httpMethod = SpringControllerUtil.resolveHttpMethodFromRequestMapping(requestMapping);
    }

    if (url == null) {
      return null;
    }

    FeignMethod feignMethod = new FeignMethod();
    feignMethod.setUrl(url);
    feignMethod.setHttpMethod(httpMethod);

    feignMethod.setName(method.getName());

    Type genericReturnType = method.getGenericReturnType();

    feignMethod.setReturnType(
        genericReturnType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));

    try {
      if (genericReturnType instanceof ParameterizedType) {
        getModelClassFromType(
            (ParameterizedType) method.getGenericReturnType(), modelClassNames);
      } else {
        String name = genericReturnType.getTypeName();
        if (name.startsWith(modelScanPath)) {
          modelClassNames.add(name);
          getModelClassFromClassName(name, modelClassNames);
        }
      }
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }

    // 解析方法参数
    List<FeignMethodArgument> arguments = new ArrayList<>();

    Parameter[] parameters = method.getParameters();

    for (Parameter parameter : parameters) {
      FeignMethodArgument feignMethodArgument = resolveFeignMethodArgumentFromParameter(parameter);
      arguments.add(feignMethodArgument);
    }

    feignMethod.setArguments(arguments);

    return feignMethod;
  }

  private FeignMethodArgument resolveFeignMethodArgumentFromParameter(Parameter parameter) {

    FeignMethodArgument argument = new FeignMethodArgument();

    String name = parameter.getName();
    Class<?> type = parameter.getType();
    Type parameterizedType = parameter.getParameterizedType();

    Annotation[] annotations = parameter.getAnnotations();
    List<String> as = new ArrayList<>();
    for (Annotation annotation1 : annotations) {
      if (annotation1 instanceof RequestBody
          || annotation1 instanceof PathVariable
          || annotation1 instanceof RequestParam) {
        as.add(annotation1.annotationType().getSimpleName());
      }
    }

    argument.setArgName(name);
    argument.setArgType(parameterizedType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));
    argument.setAnnotations(as);

    try {
      if (parameterizedType instanceof ParameterizedType) {
        getModelClassFromType(
            (ParameterizedType) parameterizedType, modelClassNames);
      } else {
        String name1 = parameterizedType.getTypeName();
        if (name1.startsWith(modelScanPath)) {
          getModelClassFromClassName(name1, modelClassNames);
          modelClassNames.add(name1);
        }
      }
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }

    String argumentClassName = type.getName();
    if (argumentClassName.startsWith(modelScanPath)) {
      modelClassNames.add(argumentClassName);
    }

    return argument;
  }

  private void getModelClassFromType(ParameterizedType type, Set<String> list)
      throws ClassNotFoundException {
    Type rawType = type.getRawType();
    Class<? extends Type> aClass = rawType.getClass();
    if (notClass(aClass)) {
      return;
    }
    String typeName = rawType.getTypeName();
    if (typeName.startsWith(modelScanPath)) {
      list.add(typeName);
    }
    Type[] actualTypeArguments = type.getActualTypeArguments();
    for (Type actualTypeArgument : actualTypeArguments) {
      if (actualTypeArgument instanceof ParameterizedType) {
        getModelClassFromType((ParameterizedType) actualTypeArgument, list);
      } else {
        String name = actualTypeArgument.getTypeName();
        if (name.startsWith(modelScanPath) && !list.contains(name)) {
          list.add(name);
          getModelClassFromClassName(name, list);
        }
      }
    }
  }

  private void getModelClassFromClassName(String className, Set<String> list)
      throws ClassNotFoundException {
    Class<?> clazz = Class.forName(className);
    if (notClass(clazz)) {
      return;
    }
    if (list.contains(className)) {
      return;
    }
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field field : declaredFields) {
      Type type = field.getGenericType();
      if (type instanceof ParameterizedType) {
        getModelClassFromType((ParameterizedType) type, list);
      } else {
        String name = type.getTypeName();
        if (name.startsWith(modelScanPath) && !list.contains(name)) {
          list.add(name);
          getModelClassFromClassName(name, list);
        }
      }
    }
  }

  private boolean notClass(Class clazz) {
    return clazz.isAnnotation() ||
        clazz.isAnonymousClass() ||
        clazz.isArray() ||
        clazz.isEnum() ||
        clazz.isInterface();
  }

  public Set<String> getModelClassNames() {
    return modelClassNames;
  }
}
